V8 엔진의 가비지 컬렉션

📅 2024. 04. 10

이 글은 Garbage Collection in V8를 읽고 핵심 내용을 요약한 글입니다. 기본적으로 브라우저가 메모리를 자동으로 정리해주고 있지만, 개발자들이 메모리 최적화에 신경쓰지 않으면 가비지 컬렉션이 자주 실행되어 사용자 경험이 저하될 수 있습니다. 브라우저의 딥한 내부 동작을 알아두면 언젠가 면접에서 써먹을 수도 있기 때문에 개발자분들은 한 번 훑고 지나가시면 좋습니다. 요약한 글의 핵심은 볼드체로 따로 강조해두었으니 참고해주세요.

  • 가비지 컬렉션의 핵심은 '죽은' 객체로부터 메모리를 확보하는 것이다. 개체에 대한 참조가 더 이상 존재하지 않는 경우, 즉 액세스할 수 없는 경우 개체는 죽은 것으로 간주된다. 따라서 이러한 객체는 더 이상 사용할 수 없으므로 지워야 한다. 반대로 '살아있는' 객체는 루트에서 참조를 통해 도달할 수 있는 객체이다.
  • V8 엔진은 자바스크립트 객체 뿐만 아니라 제거된 HTML 요소, 사용하지 않은 CSS 스타일시트, 완성된 애니메이션 등 수집할 수 있는 모든 것에 가비지 컬렉션을 제공한다.
  • 생성된 모든 객체에 대한 참조는 GlobalGCInfoTable이라는 별도의 글로벌 테이블에 저장된다. 가비지 수집 중에는 루트에서 객체의 반복 탐색이 수행된다.
  • 죽은 개체를 찾아서 정리하는 절차는 상당히 느릴 수 있다. 최신 애플리케이션에서 메모리는 크기가 1Gb를 초과하는 수천 개의 객체를 포함할 수 있다. 자바스크립트의 경우 단일 스레드이고 스레드 간 동기화 메커니즘이 없기 때문에 전체 객체 추적을 정기적으로 수행하면 지연과 성능 저하가 발생할 가능성이 높다.
  • 시스템 과부하를 방지하기 위해 V8 팀은 가비지 컬렉션 프로세스를 여러 부분으로 나누고 각각 개별적으로 작업하기로 결정했다. 세대 가설(generational hypothesis)이라는 가설이 있는데, 이에 따르면 대부분의 경우 어린 객체는 오래된 객체보다 더 빨리 죽을 가능성이 높다. 따라서 V8에서는 세대라는 개념이 도입되었다. 젊은 세대(young generation)노년 세대(old generation) 가 있다.
  • 새로 할당된 객체들은 젊은 세대로 구분되어 힙에 배치된다. 오래된 객체들은 노년 세대로 구분되며 최대 1.4Gb 크기까지 가질 수 있다.
  • 젊은 세대는 활성(active)과 비활성(inactive)이라는 두 개의 반쪽(세미-스페이스)으로 나뉜다. 새로운 객체들은 항상 현재 활성인 세미-스페이스에 배치된다.
  • 활성 세미-스페이스가 가득 차면, '스캐빈저(Scavenger)' 메커니즘이 작동한다. 이 메커니즘은 살아있는 객체들을 확인하고, 그것들을 비활성 세미-스페이스로 이동시키는 작업을 한다. 이 과정을 마이너 가비지 컬렉션(Minor Garbage Collection)이라고 한다. 작업이 끝나면, 두 세미-스페이스의 역할이 바뀐다. 즉, 이전에 활성이었던 세미-스페이스는 비활성화되고, 비활성이었던 세미-스페이스는 클리어되어 활성화된다.
  • 만약 객체들이 이미 한 번 이동되어 '노년 세대'로 분류될 준비가 되었다면, 다음 마이너 가비지 컬렉션 실행 시, 그 객체들은 '노년 세대'로 이동된다. 반면, 필요 없어진 객체들은 삭제된다.
  • 간단히 말해, JavaScript는 메모리 내의 객체들을 효율적으로 관리하기 위해, 새로운 객체를 활성 세미-스페이스에 배치하고, 필요할 때 비활성 세미-스페이스로 이동시키며, 더 이상 필요 없는 객체를 제거하는 과정을 반복한다. 이 모든 과정은 단일 스레드 내에서 실행된다.
  • 노년 세대의 메모리 정리는 '메이저 가비지 컬렉션(Major Garbage Collection)'이라는 다른 과정을 통해 이루어진다. 이 과정은 젊은 세대의 가비지 컬렉션과는 달리, 더 큰 메모리 영역에서 더 오래 살아남은 객체들을 대상으로 한다.
  • 메이저 가비지 컬렉션(이하 메이저 GC)은 노년 세대의 크기가 가득 차거나, 메모리가 높아졌을 때 실행된다.
  • 메이저 GC는 보통 마이너 GC보다 더 많은 시간이 소요된다. 메이저 GC가 실행되는 동안, JavaScript 엔진은 애플리케이션의 실행을 일시적으로 멈출 수 있다. 이를 'Stop-The-World' 중단이라고 한다. 중단 시간은 가능한 짧게 유지되어야 하지만, 메이저 GC가 수행되는 동안은 애플리케이션응답 속도가 느려질 수 있다.
  • 개발자들이 메모리 관리를 잘하면 메이저 GC의 빈도를 줄일 수 있다.
  • V8 엔진은 메모리 리듀서를 통해 메이저 GC의 빈도를 줄이고 있다. 메모리 리듀서는 웹 페이지가 비활성화 상태(ex: 사용자와 상호작용이 없는 상태)일 때 메이저 GC를 실행함으로써, 사용자가 웹 페이지를 사용하는 도중에 발생하는 성능 저하를 줄인다.